﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Habanero.Base;
using Habanero.BO;

namespace FireStarter.Base.BusinessObjectMerger
{
    public  class CompareFailureMessage : BOError
    {
        public CompareFailureMessage(string message) : base(message, ErrorLevel.Error)
        {
        }
    }

    public class MergeBO<T> :IMergeBO<T> where T: class, IMergableBo 
    {
        private readonly CompareResult _result = new CompareResult(new List<IBOError>());

        public virtual IMergableBo MergeBOFrom(IMergableBo fromBO, IMergableBo toBO)
        {
            return this.MergeBOFrom((T) fromBO, (T) toBO);
        }

        public virtual T MergeBOFrom(T fromBO, T toBO)
        {
            if (toBO == null)
            {
                toBO = this.CreateBusinessObject();
            }
            this.MergeProperties(fromBO, toBO);
            this.CopyRelationships(fromBO, toBO);
            return toBO;
        }
        protected virtual void CopyRelationships(T fromBO, T toBO)
        {
            if (fromBO == null) return;

            foreach (var relationship in fromBO.Relationships)
            {
                if (MustMergeRelationship(relationship))
                {
                    if ((relationship is IMultipleRelationship))
                    {
                        IMultipleRelationship fromMultipleRelationship = (IMultipleRelationship)relationship;
                        IMultipleRelationship toMultipleRelationship = GetMultipleRelationship(toBO, fromMultipleRelationship.RelationshipName);
                        MergeMultipleRelationship(fromMultipleRelationship, toMultipleRelationship);
                    }
                    else if ((relationship is ISingleRelationship))
                    {
                        ISingleRelationship fromSingleRelationship = (ISingleRelationship)relationship;
                        ISingleRelationship toSingleRelationship = GetSingleRelationship(toBO, fromSingleRelationship.RelationshipName);
                        MergeSingleRelationship(fromSingleRelationship, toSingleRelationship);
                    }
                }
            }
        }

        #region Compare

        protected virtual CompareResult CompareSingleRelationship(ISingleRelationship fromSingleRelationship, ISingleRelationship toSingleRelationship)
        {
            if (toSingleRelationship == null || fromSingleRelationship == null) return new CompareResult("Either the Single ToRelationship or the FromRelationshp is null");
            var mergeRelatedBO = GetMerger(fromSingleRelationship.RelatedObjectClassDef);

            var fromBO = fromSingleRelationship.GetRelatedObject() as IMergableBo;
            var toBo = toSingleRelationship.GetRelatedObject() as IMergableBo;
            return mergeRelatedBO.CompareBusinessObjects(fromBO, toBo);
        }

        protected virtual CompareResult CompareMultipleRelationships(IMultipleRelationship fromMultipleRelationship, IMultipleRelationship toMultipleRelationship)
        {
            if (toMultipleRelationship != null)
            {
                var fromBOCol = fromMultipleRelationship.BusinessObjectCollection;
                var toBoCol = toMultipleRelationship.BusinessObjectCollection;
                if (fromBOCol.Count != toBoCol.Count)
                {
                    _result.AddFailure(fromMultipleRelationship + @".Count " + fromBOCol.Count + @" != "
                        + toMultipleRelationship + @".Count " + toBoCol.Count);
                    return _result;
                }
                return CompareBoCol(fromBOCol, toBoCol);
            }
            return _result;
        }

        protected virtual CompareResult CompareBoCol(IBusinessObjectCollection fromBOCol, IBusinessObjectCollection toBoCol)
        {
            IMergeBO mergeRelatedBO = GetMerger(fromBOCol.ClassDef);
            foreach (IMergableBo fromBO in GetBusinessObjectArray(fromBOCol))
            {
                var toBO = mergeRelatedBO.FindBusinessObject(toBoCol, fromBO);
                if (toBO == null)
                {
                    _result.AddFailure(new CompareFailureMessage("There is no matching BO for " + fromBO));
                    continue;
                }
                _result.AddResult(mergeRelatedBO.CompareBusinessObjects(fromBO, toBO));
            }
            return _result;
        }

        protected virtual CompareResult CompareProps(IBusinessObject fromMergableBo, IBusinessObject toMergableBo)
        {
            if (fromMergableBo == null && toMergableBo == null) return _result;
            if (fromMergableBo == null || toMergableBo == null)
            {
                _result.AddFailure(fromMergableBo == null
                                         ? new CompareFailureMessage("fromMergableBO is null")
                                         : new CompareFailureMessage("toMergableBO is null"));
                return _result;
            }

            foreach (IBOProp prop in fromMergableBo.Props)
            {
                // Update Properties of existing (To) properties to match updated Props (From)
                if (!MustMergeProperty(prop)) continue;
                IBOProp toProp = toMergableBo.Props[prop.PropertyName];
                if (IsNotEqual(prop, toProp))
                {
                    _result.AddFailure(
                        new CompareFailureMessage(string.Format("{0}='{1}' is not equal to {2}='{3}'", prop.DisplayName, prop.Value, toProp.DisplayName, toProp.Value)));
                }
            }
            return _result;
        }

        protected virtual CompareResult CompareRelationships(T x, T y)
        {
            foreach (var relationship in x.Relationships)
            {
                if (MustMergeRelationship(relationship))
                {
                    if ((relationship is IMultipleRelationship))
                    {
                        var fromMultipleRelationship = (IMultipleRelationship)relationship;
                        var toMultipleRelationship = GetMultipleRelationship(y, fromMultipleRelationship.RelationshipName);
                        return CompareMultipleRelationships(fromMultipleRelationship, toMultipleRelationship);
                    }
                    if ((relationship is ISingleRelationship))
                    {
                        var fromSingleRelationship = (ISingleRelationship)relationship;
                        var toSingleRelationship = GetSingleRelationship(y, fromSingleRelationship.RelationshipName);
                        return CompareSingleRelationship(fromSingleRelationship, toSingleRelationship);
                    }
                }
            }
            return _result;
        }

        public CompareResult CompareBusinessObjects(IMergableBo x, IMergableBo y)
        {
            return CompareBusinessObjects((T)x, (T)y);
        }

        public virtual CompareResult CompareBusinessObjects(T x, T y)
        {

            if (x == null && y == null) return _result;

            if (x == null ||y ==null)
            {
                _result.AddFailure("Either the X or the Y business object is null");
            }

            if (_result.AreEqual)
            {
                CompareProps(x, y);
                CompareRelationships(x, y);
            }
            return _result;
        }

        private static bool IsNotEqual(IBOProp prop, IBOProp toProp)
        {
            return !(new BoPropComparer().Equals(prop, toProp));
        }

        #endregion

        protected virtual void MergeMultipleRelationship(IMultipleRelationship fromMultipleRelationship, IMultipleRelationship toMultipleRelationship)
        {
            if (toMultipleRelationship != null)
            {
                var fromBOCol = fromMultipleRelationship.BusinessObjectCollection;
                var toBoCol = toMultipleRelationship.BusinessObjectCollection;
                MergeBoCol(fromBOCol, toBoCol);
            }
        }

        protected virtual void MergeBoCol(IBusinessObjectCollection fromBOCol, IBusinessObjectCollection toBoCol)
        {
            IMergeBO mergeRelatedBO = GetMerger(fromBOCol.ClassDef);
            foreach (IMergableBo toRelatedBO in GetBusinessObjectArray(toBoCol))
            {
                IBusinessObject foundRelatedBOFrom = mergeRelatedBO.FindBusinessObject(fromBOCol, toRelatedBO);
                var changeType = foundRelatedBOFrom == null ? ChangeType.Delete : ChangeType.None;
                if (toRelatedBO.PendingChangeType == ChangeType.Add && foundRelatedBOFrom != null) continue;
                SetPendingTypeChange(toRelatedBO, changeType);
            }
            foreach (IMergableBo fromRelatedBO in GetBusinessObjectArray(fromBOCol))
            {

                IMergableBo foundRelatedBO = mergeRelatedBO.FindBusinessObject(toBoCol, fromRelatedBO);
                if(foundRelatedBO == null)
                {
                    foundRelatedBO = mergeRelatedBO.CreateBusinessObject();
                    toBoCol.Add(foundRelatedBO);
                }
                foundRelatedBO = mergeRelatedBO.MergeBOFrom(fromRelatedBO, foundRelatedBO);
                toBoCol.Add(foundRelatedBO);
            }
        }

        private static IMergeBO GetMerger(IClassDef classDef)
        {
            return MergeableBORegistry.Instance.Resolve(classDef.ClassType);
        }

        private IBusinessObject[] GetBusinessObjectArray(IBusinessObjectCollection fromBOCol)
        {
            IBusinessObject[] businessObjects = new IBusinessObject[fromBOCol.Count];
            fromBOCol.CopyTo(businessObjects, 0);
            return businessObjects;
        }

        protected virtual void MergeSingleRelationship(ISingleRelationship fromSingleRelationship, ISingleRelationship toSingleRelationship)
        {
            if(toSingleRelationship == null || fromSingleRelationship == null) return;
            if (fromSingleRelationship.GetRelatedObject() != null)
            {
                IBusinessObject toRelatedBO = GetMergedRelatedBO(fromSingleRelationship, toSingleRelationship);
                toSingleRelationship.SetRelatedObject(toRelatedBO);
            }else
            {
                IMergableBo relatedObject = toSingleRelationship.GetRelatedObject() as IMergableBo;
                if(relatedObject != null)
                {
                    relatedObject.PendingChangeType = ChangeType.Delete;
                }
            }
        }

        private IMultipleRelationship GetMultipleRelationship(T toBO, string relationshipName)
        {
            return GetRelationship(toBO, relationshipName) as IMultipleRelationship;
        }

        private static IRelationship GetRelationship(T toBO, string relationshipName)
        {
            return toBO.Relationships[relationshipName];
        }

        private static ISingleRelationship GetSingleRelationship(T toBO, string relationshipName)
        {
            return GetRelationship(toBO, relationshipName) as ISingleRelationship;
        }

        private IBusinessObject GetMergedRelatedBO(ISingleRelationship fromSingleRelationship, ISingleRelationship toSingleRelationship)
        {
            IMergeBO mergeBO =
                MergeableBORegistry.Instance.Resolve(RelatedClassType(toSingleRelationship));
            IBusinessObject toRelatedObject = toSingleRelationship.GetRelatedObject();
            var fromRelatedBO = fromSingleRelationship.GetRelatedObject();
            if (toRelatedObject == null && fromRelatedBO != null)
            {
                toRelatedObject = mergeBO.CreateBusinessObject();
                toSingleRelationship.SetRelatedObject(toRelatedObject);
            }
            return mergeBO.MergeBOFrom((IMergableBo)fromRelatedBO, (IMergableBo)toRelatedObject);
        }

        private static Type RelatedClassType(IRelationship toSingleRelationship)
        {
            return toSingleRelationship.RelatedObjectClassDef.ClassType;
        }

        public virtual T CreateBusinessObject()
        {
            var tmpBO = (T) Activator.CreateInstance(typeof (T));
            SetPendingTypeChange(tmpBO, ChangeType.Add);
            tmpBO.PendingChangeApproved = true;
            return tmpBO;
        }

        public void UpdatePendingChangeType(IMergableBo parent)
        {
            this.UpdatePendingChangeType((T)parent);
        }


        public void UpdatePendingChangeType(T parent)
        {

            if (parent == null) return;
            if(parent.PendingChangeType != ChangeType.None && parent.PendingChangeType != 0)
            {
                //parent.PendingChangeApproved = true;
            }
            if (parent.Status.IsDirty && (parent.PendingChangeType == ChangeType.None || parent.PendingChangeType == 0))
            {
                parent.PendingChangeType = ChangeType.Update;
              //  parent.PendingChangeApproved = true;
            }
            foreach (var relationship in parent.Relationships.Where(MustMergeRelationship))
            {
                UpdateParentChangeType(parent, relationship);
            }
        }

        private void UpdateParentChangeType(IMergableBo parent, IRelationship relationship)
        {
            if ((relationship is IMultipleRelationship))
            {
                IMultipleRelationship multipleRelationship = (IMultipleRelationship)relationship;

                var boCol = multipleRelationship.BusinessObjectCollection;
                UpdateParentChangeType(parent, boCol);
            }
            else if ((relationship is ISingleRelationship))
            {
                ISingleRelationship singleRelationship = (ISingleRelationship)relationship;
                var childBO = singleRelationship.GetRelatedObject() as IMergableBo;
                if (childBO != null && childBO.PendingChangeType != ChangeType.None)
                {
                    if (parent.PendingChangeType != ChangeType.Delete && parent.PendingChangeType != ChangeType.Add)
                    {
                        parent.PendingChangeType = ChangeType.Update;
                       // parent.PendingChangeApproved = true;
                    }
                }
                if (childBO != null)
                {
                    var mergeBO = GetMerger(childBO.ClassDef);
                    mergeBO.UpdatePendingChangeType(childBO);
                }
            }
        }

        private void UpdateParentChangeType(IMergableBo parentMergableBo, IBusinessObjectCollection childBoCol)
        {
            if (parentMergableBo.PendingChangeType == ChangeType.None || parentMergableBo.PendingChangeType == 0)
            {
                if (childBoCol.Cast<IMergableBo>().Any(mergableBo => mergableBo.PendingChangeType != ChangeType.None))
                {
                    parentMergableBo.PendingChangeType = ChangeType.Update;
                   // parentMergableBo.PendingChangeApproved = true;
                }
            }
            //TODO brett 08 Sep 2010: Write tests for this
            var mergeBO = GetMerger(childBoCol.ClassDef);
            foreach (IMergableBo childBO in GetBusinessObjectArray(childBoCol))
            {
                mergeBO.UpdatePendingChangeType(childBO);
            }
        }
        protected virtual void SetPendingTypeChange(IBusinessObject businessObject, ChangeType changeType)
        {
            IMergableBo mergableBo = businessObject as IMergableBo;
            if (mergableBo != null)
            {
                mergableBo.PendingChangeType = changeType;
            }
        }

        protected virtual void MergeProperties(IBusinessObject fromMergableBo, IBusinessObject toMergableBo)
        {
            if (fromMergableBo == null) return;
            foreach (IBOProp prop in fromMergableBo.Props)
            {
                // Update Properties of existing (To) properties to match updated Props (From)
                if (!MustMergeProperty(prop)) continue;
                IBOProp toProp = toMergableBo.Props[prop.PropertyName];
                toProp.Value = prop.Value;
            }
        }

        /// <summary>
        /// Find a Business Object in the collection that matches the Find Predicate
        /// </summary>
        /// <param name="businessObjectCollection"></param>
        /// <param name="boToFind"></param>
        /// <returns></returns>
        public IMergableBo FindBusinessObject(IBusinessObjectCollection businessObjectCollection, IMergableBo boToFind)
        {
            return this.FindBusinessObject(businessObjectCollection, (T)boToFind);
        }

        IMergableBo IMergeBO.CreateBusinessObject()
        {
            return CreateBusinessObject();
        }

        public virtual T FindBusinessObject(IBusinessObjectCollection businessObjectCollection, T boToFind)
        {
            Func<T, bool> matchBusinessObject = MatchBusinessObject(boToFind);
            return GetBusinessObjectArray(businessObjectCollection)
                .Cast<T>()
                .FirstOrDefault(matchBusinessObject);
        }

        public virtual Func<T, bool> MatchBusinessObject(T boToFind)
        {
            return foundBO => boToFind != null && foundBO != null && foundBO.ToString() == boToFind.ToString();
        }

        public virtual bool MustMergeProperty(IBOProp prop)
        {
            return prop.PropertyType != typeof(Guid) 
                && prop.PropertyType != typeof(ChangeType)
                && prop.PropertyName != "Generated"
                && prop.PropertyName != "GenerateCode" 
                && prop.PropertyName != "NeedsRegeneration"
                && prop.PropertyName != "PendingChangeApproved";
        }

        public virtual bool MustMergeRelationship(IRelationship relationship)
        {
            RelationshipType relationshipType = relationship.RelationshipDef.RelationshipType;
            return relationshipType != RelationshipType.Association;
        }
    }

}
